<!DOCTYPE html>
<html>
<!--
Copyright 2008 The Closure Library Authors. All Rights Reserved.

Use of this source code is governed by an Apache 2.0 License.
See the COPYING file for details.
-->
<head>
  <!-- Author:  gboyer@google.com (Garrett Boyer) -->
  <title>Closure Unit Tests - goog.ui.KeyboardShortcutHandler</title>
  <script src="../base.js"></script>
  <script>
    goog.require('goog.dom');
    goog.require('goog.testing.MockClock');
    goog.require('goog.testing.PropertyReplacer');
    goog.require('goog.testing.StrictMock');
    goog.require('goog.testing.events');
    goog.require('goog.testing.jsunit');
    goog.require('goog.ui.KeyboardShortcutHandler');
  </script>
</head>
<body>
  <div id="rootDiv">
    <div id="targetDiv">The test presses keys on me!</div>
    <input type="text" id="targetTextBox" value="text box">
    <input type="password" id="targetPassword" value="password">
    <input type="checkbox" id="targetCheckBox">
    <textarea id="targetTextArea">text area</textarea>
    <select id="targetSelect"><option>select</option></select>
    <button id="targetButton">A Button</button>
  </div>
  <script>
    var Modifiers = goog.ui.KeyboardShortcutHandler.Modifiers;
    var KeyCodes = goog.events.KeyCodes;

    var handler;
    var targetDiv;
    var listener;
    var mockClock;
    var stubs = new goog.testing.PropertyReplacer();

    /**
     * Fires a keypress on the target div.
     * @return {boolean} The returnValue of the sequence: false if
     *     preventDefault() was called on any of the events, true otherwise.
     */
    function fire(keycode, opt_extraProperties, opt_element) {
      return goog.testing.events.fireKeySequence(
          opt_element || targetDiv, keycode, opt_extraProperties);
    }

    function fireAltGraphKey(keycode, keyPressKeyCode, opt_extraProperties,
                             opt_element) {
      return goog.testing.events.fireNonAsciiKeySequence(
          opt_element || targetDiv, keycode, keyPressKeyCode,
          opt_extraProperties);
    }

    function setUp() {
      targetDiv = goog.dom.getElement('targetDiv');
      handler = new goog.ui.KeyboardShortcutHandler(
          goog.dom.getElement('rootDiv'));

      // Create a mock event listener in order to set expectations on what
      // events are fired.  We create a fake class whose only method is
      // shortcutFired(shortcut identifier).
      listener = new goog.testing.StrictMock(
          {shortcutFired: goog.nullFunction});
      goog.events.listen(
          handler,
          goog.ui.KeyboardShortcutHandler.EventType.SHORTCUT_TRIGGERED,
          function (event) { listener.shortcutFired(event.identifier); });

       // Set up a fake clock, because keyboard shortcuts *are* time
       // sensitive.
       mockClock = new goog.testing.MockClock(true);
    }

    function tearDown() {
      mockClock.uninstall();
      handler.dispose();
      stubs.reset();
    }

    function testAllowsSingleLetterKeyBindingsSpecifiedAsString() {
      listener.shortcutFired('lettergee');
      listener.$replay();

      handler.registerShortcut('lettergee', 'g');
      fire(KeyCodes.G);

      listener.$verify()
    }

    function testAllowsSingleLetterKeyBindingsSpecifiedAsKeyCode() {
      listener.shortcutFired('lettergee');
      listener.$replay();

      handler.registerShortcut('lettergee', KeyCodes.G);
      fire(KeyCodes.G);

      listener.$verify()
    }

    function testDoesntFireWhenWrongKeyIsPressed() {
      listener.$replay(); // no events expected

      handler.registerShortcut('letterjay', 'j');
      fire(KeyCodes.G);

      listener.$verify()
    }

    function testAllowsControlAndLetterSpecifiedAsAString() {
      listener.shortcutFired('lettergee');
      listener.$replay();

      handler.registerShortcut('lettergee', 'ctrl+g');
      fire(KeyCodes.G, {ctrlKey: true});

      listener.$verify()
    }

    function testAllowsControlAndLetterSpecifiedAsArgSequence() {
      listener.shortcutFired('lettergeectrl');
      listener.$replay();

      handler.registerShortcut('lettergeectrl',
          KeyCodes.G, Modifiers.CTRL);
      fire(KeyCodes.G, {ctrlKey: true});

      listener.$verify()
    }

    function testAllowsControlAndLetterSpecifiedAsArray() {
      listener.shortcutFired('lettergeectrl');
      listener.$replay();

      handler.registerShortcut('lettergeectrl',
          [KeyCodes.G, Modifiers.CTRL]);
      fire(KeyCodes.G, {ctrlKey: true});

      listener.$verify()
    }

    function testAllowsShift() {
      listener.shortcutFired('lettergeeshift');
      listener.$replay();

      handler.registerShortcut('lettergeeshift',
          [KeyCodes.G, Modifiers.SHIFT]);
      fire(KeyCodes.G, {shiftKey: true});

      listener.$verify()
    }

    function testAllowsAlt() {
      listener.shortcutFired('lettergeealt');
      listener.$replay();

      handler.registerShortcut('lettergeealt',
          [KeyCodes.G, Modifiers.ALT]);
      fire(KeyCodes.G, {altKey: true});

      listener.$verify()
    }

    function testAllowsMeta() {
      listener.shortcutFired('lettergeemeta');
      listener.$replay();

      handler.registerShortcut('lettergeemeta',
          [KeyCodes.G, Modifiers.META]);
      fire(KeyCodes.G, {metaKey: true});

      listener.$verify()
    }

    function testAllowsMultipleModifiers() {
      listener.shortcutFired('lettergeectrlaltshift');
      listener.$replay();

      handler.registerShortcut('lettergeectrlaltshift',
          KeyCodes.G, Modifiers.CTRL | Modifiers.ALT | Modifiers.SHIFT);
      fire(KeyCodes.G, {ctrlKey: true, altKey: true, shiftKey: true});

      listener.$verify()
    }

    function testAllowsMultipleModifiersSpecifiedAsString() {
      listener.shortcutFired('lettergeectrlaltshiftmeta');
      listener.$replay();

      handler.registerShortcut('lettergeectrlaltshiftmeta',
          'ctrl+shift+alt+meta+g');
      fire(KeyCodes.G,
          {ctrlKey: true, altKey: true, shiftKey: true, metaKey: true});

      listener.$verify()
    }

    function testPreventsDefaultOnReturnFalse() {
      listener.shortcutFired('x');
      listener.$replay();

      handler.registerShortcut('x', 'x');
      var key = goog.events.listen(handler,
          goog.ui.KeyboardShortcutHandler.EventType.SHORTCUT_TRIGGERED,
          function (event) { return false });

      assertFalse('return false in listener must prevent default',
                  fire(KeyCodes.X));

      listener.$verify();

      goog.events.unlistenByKey(key);
    }

    function testPreventsDefaultWhenExceptionThrown() {
      handler.registerShortcut('x', 'x');
      handler.setAlwaysPreventDefault(true);
      goog.events.listenOnce(handler,
          goog.ui.KeyboardShortcutHandler.EventType.SHORTCUT_TRIGGERED,
          function (event) { throw new Error('x'); });

      // We can't use the standard infrastructure to detect that
      // the event was preventDefaulted, because of the exception.
      var callCount = 0;
      stubs.set(goog.events.BrowserEvent.prototype, 'preventDefault',
          function() {
        callCount++;
      });

      try {
        fire(KeyCodes.X);
        fail('Expected exception');
      } catch (e) {
        assertContains(e.message, 'x');
      }

      assertEquals(1, callCount);
    }

    function testDoesntFireWhenUserForgetsRequiredModifier() {
      listener.$replay(); // no events expected

      handler.registerShortcut('lettergeectrl',
          KeyCodes.G, Modifiers.CTRL);
      fire(KeyCodes.G);

      listener.$verify()
    }

    function testDoesntFireIfTooManyModifiersPressed() {
      listener.$replay(); // no events expected

      handler.registerShortcut('lettergeectrl',
          KeyCodes.G, Modifiers.CTRL);
      fire(KeyCodes.G, {ctrlKey: true, metaKey: true});

      listener.$verify()
    }

    function testDoesntFireIfAnyRequiredModifierForgotten() {
      listener.$replay(); // no events expected

      handler.registerShortcut('lettergeectrlaltshift',
          KeyCodes.G, Modifiers.CTRL | Modifiers.ALT | Modifiers.SHIFT);
      fire(KeyCodes.G, {altKey: true, shiftKey: true});

      listener.$verify()
    }

    function testAllowsMultiKeySequenceSpecifiedAsArray() {
      listener.shortcutFired('quitemacs');
      listener.$replay();

      handler.registerShortcut('quitemacs',
          [KeyCodes.X, Modifiers.CTRL,
           KeyCodes.C]);
      fire(KeyCodes.X, {ctrlKey: true});
      fire(KeyCodes.C);

      listener.$verify()
    }

    function testAllowsMultiKeySequenceSpecifiedAsArray() {
      listener.shortcutFired('quitemacs');
      listener.$replay();

      handler.registerShortcut('quitemacs',
          [KeyCodes.X, Modifiers.CTRL,
           KeyCodes.C]);
      fire(KeyCodes.X, {ctrlKey: true});
      fire(KeyCodes.C);

      listener.$verify()
    }

    function testAllowsMultiKeySequenceSpecifiedAsArguments() {
      listener.shortcutFired('quitvi');
      listener.$replay();

      handler.registerShortcut('quitvi',
          KeyCodes.COLON, Modifiers.NONE,
          KeyCodes.Q, Modifiers.NONE,
          KeyCodes.EXCLAMATION, Modifiers.NONE);
      fire(KeyCodes.COLON);
      fire(KeyCodes.Q);
      fire(KeyCodes.EXCLAMATION);

      listener.$verify()
    }

    function testMultiKeyEventIsNotFiredIfUserIsTooSlow() {
      listener.$replay(); // no events expected

      handler.registerShortcut('quitemacs',
          [KeyCodes.X, Modifiers.CTRL,
           KeyCodes.C]);

      fire(KeyCodes.X, {ctrlKey: true});

      // Wait 3 seconds before hitting C.  Although the actual limit is 1500
      // at time of writing, it's best not to over-specify functionality.
      mockClock.tick(3000);

      fire(KeyCodes.C);

      listener.$verify()
    }

    function testAllowsMultipleAHandlers() {
      listener.shortcutFired('quitvi');
      listener.shortcutFired('letterex');
      listener.shortcutFired('quitemacs');
      listener.$replay();

      // register 3 handlers in 3 diferent ways
      handler.registerShortcut('quitvi',
          KeyCodes.COLON, Modifiers.NONE,
          KeyCodes.Q, Modifiers.NONE,
          KeyCodes.EXCLAMATION, Modifiers.NONE);
      handler.registerShortcut('quitemacs',
          [KeyCodes.X, Modifiers.CTRL,
           KeyCodes.C]);
      handler.registerShortcut('letterex', 'x');


      // quit vi
      fire(KeyCodes.COLON);
      fire(KeyCodes.Q);
      fire(KeyCodes.EXCLAMATION);

      // then press the letter x
      fire(KeyCodes.X);

      // then quit emacs
      fire(KeyCodes.X, {ctrlKey: true});
      fire(KeyCodes.C);

      listener.$verify()
    }

    function testCanRemoveOneHandler() {
      listener.shortcutFired('letterex');
      listener.$replay();

      // register 2 handlers, then remove quitvi
      handler.registerShortcut('quitvi',
          KeyCodes.COLON, Modifiers.NONE,
          KeyCodes.Q, Modifiers.NONE,
          KeyCodes.EXCLAMATION, Modifiers.NONE);
      handler.registerShortcut('letterex', 'x');
      handler.unregisterShortcut(
          KeyCodes.COLON, Modifiers.NONE,
          KeyCodes.Q, Modifiers.NONE,
          KeyCodes.EXCLAMATION, Modifiers.NONE);

      // call the "quit VI" keycodes, even though it is removed
      fire(KeyCodes.COLON);
      fire(KeyCodes.Q);
      fire(KeyCodes.EXCLAMATION);

      // press the letter x
      fire(KeyCodes.X);

      listener.$verify()
    }

    function testCanRemoveTwoHandlers() {
      listener.$replay(); // no events expected

      handler.registerShortcut('quitemacs',
          [KeyCodes.X, Modifiers.CTRL,
           KeyCodes.C]);
      handler.registerShortcut('letterex', 'x');
      handler.unregisterShortcut(
          [KeyCodes.X, Modifiers.CTRL,
           KeyCodes.C]);
      handler.unregisterShortcut('x');

      fire(KeyCodes.X, {ctrlKey: true});
      fire(KeyCodes.C);
      fire(KeyCodes.X);

      listener.$verify()
    }

    function testCheckRegisteredShortcuts() {
      assertFalse(handler.isShortcutRegistered('x'));
      handler.registerShortcut('letterex', 'x');
      assertTrue(handler.isShortcutRegistered('x'));

      handler.registerShortcut('qe', [KeyCodes.X, Modifiers.CTRL, KeyCodes.C]);
      assertTrue(handler.isShortcutRegistered('x'));
      assertTrue(handler.isShortcutRegistered([KeyCodes.X,
                                               Modifiers.CTRL, KeyCodes.C]));
      handler.unregisterShortcut('x');
      assertFalse(handler.isShortcutRegistered('x'));
      assertTrue(handler.isShortcutRegistered([KeyCodes.X,
                                               Modifiers.CTRL, KeyCodes.C]));
      handler.unregisterShortcut([KeyCodes.X, Modifiers.CTRL, KeyCodes.C]);
      assertFalse(handler.isShortcutRegistered([KeyCodes.X,
                                                Modifiers.CTRL, KeyCodes.C]));
    }

    /**
     * Registers a slew of keyboard shortcuts to test each primary category
     * of shortcuts.
     */
    function registerEnterSpaceXF1AltY() {
      // Enter and space are specially handled keys.
      handler.registerShortcut('enter', KeyCodes.ENTER);
      handler.registerShortcut('space', KeyCodes.SPACE)
      // 'x' should be treated as text in many contexts
      handler.registerShortcut('x', 'x');
      // F1 is a global shortcut.
      handler.registerShortcut('global', KeyCodes.F1);
      // Alt-Y has modifiers, which pass through most form elements.
      handler.registerShortcut('withAlt', 'alt+y');
    }

    /**
     * Fires enter, space, X, F1, and Alt-Y keys on a widget.
     * @param {Element} target The element on which to fire the events.
     */
    function fireEnterSpaceXF1AltY(target) {
      fire(KeyCodes.ENTER, undefined, target);
      fire(KeyCodes.SPACE, undefined, target);
      fire(KeyCodes.X, undefined, target);
      fire(KeyCodes.F1, undefined, target);
      fire(KeyCodes.Y, {altKey: true}, target);
    }

    function testIgnoreNonGlobalShortcutsInSelect() {
      var targetSelect = goog.dom.getElement('targetSelect');

      listener.shortcutFired('global');
      listener.shortcutFired('withAlt');
      listener.$replay();

      registerEnterSpaceXF1AltY();
      fireEnterSpaceXF1AltY(goog.dom.getElement('targetSelect'));

      listener.$verify();
    }

    function testIgnoreNonGlobalShortcutsInTextArea() {
      listener.shortcutFired('global');
      listener.shortcutFired('withAlt');
      listener.$replay();

      registerEnterSpaceXF1AltY();
      fireEnterSpaceXF1AltY(goog.dom.getElement('targetTextArea'));

      listener.$verify();
    }

   function testIgnoreShortcutsExceptEnterInTextBoxAndPassword() {
      // Events for the text box.
      listener.shortcutFired('enter');
      listener.shortcutFired('global');
      listener.shortcutFired('withAlt');
      // Events for the password field.
      listener.shortcutFired('enter');
      listener.shortcutFired('global');
      listener.shortcutFired('withAlt');
      listener.$replay();

      registerEnterSpaceXF1AltY();
      fireEnterSpaceXF1AltY(goog.dom.getElement('targetTextBox'));
      fireEnterSpaceXF1AltY(goog.dom.getElement('targetPassword'));

      listener.$verify();
    }

    function testIgnoreSpaceInCheckBoxAndButton() {
      // Events for the check box.
      listener.shortcutFired('enter');
      listener.shortcutFired('x');
      listener.shortcutFired('global');
      listener.shortcutFired('withAlt');
      // Events for the button.
      listener.shortcutFired('enter');
      listener.shortcutFired('x');
      listener.shortcutFired('global');
      listener.shortcutFired('withAlt');
      listener.$replay();

      registerEnterSpaceXF1AltY();
      fireEnterSpaceXF1AltY(goog.dom.getElement('targetCheckBox'));
      fireEnterSpaceXF1AltY(goog.dom.getElement('targetButton'));

      listener.$verify();
    }

    function testIgnoreNonGlobalShortcutsInContentEditable() {
      try {
        document.designMode = 'on';
        targetDiv.contentEditable = 'true';

        // Expect only global shortcuts.
        listener.shortcutFired('global');
        listener.$replay();

        registerEnterSpaceXF1AltY();
        fireEnterSpaceXF1AltY(targetDiv);

        listener.$verify();
      } finally {
        document.designMode = 'off';
        targetDiv.contentEditable = 'false';
      }
    }

    function testSetAllShortcutsAreGlobal() {
      listener.shortcutFired('enter');
      listener.shortcutFired('space');
      listener.shortcutFired('x');
      listener.shortcutFired('global');
      listener.shortcutFired('withAlt');
      listener.$replay();

      registerEnterSpaceXF1AltY();
      handler.setAllShortcutsAreGlobal(true);
      fireEnterSpaceXF1AltY(goog.dom.getElement('targetTextArea'));

      listener.$verify();
    }

    function testSetModifierShortcutsAreGlobalFalse() {
      listener.shortcutFired('global');
      listener.$replay();

      registerEnterSpaceXF1AltY();
      handler.setModifierShortcutsAreGlobal(false);
      fireEnterSpaceXF1AltY(goog.dom.getElement('targetTextArea'));

      listener.$verify();
    }

    function testAltGraphKeyOnUSLayout() {
      // Windows does not assign printable characters to any ctrl+alt keys of
      // the US layout. This test verifies we fire shortcut events when typing
      // ctrl+alt keys on the US layout.
      listener.shortcutFired('letterOne');
      listener.shortcutFired('letterTwo');
      listener.shortcutFired('letterThree');
      listener.shortcutFired('letterFour');
      listener.shortcutFired('letterFive');
      if (goog.userAgent.WINDOWS && !goog.userAgent.GECKO) {
        listener.$replay();

        handler.registerShortcut('letterOne', 'ctrl+alt+1');
        handler.registerShortcut('letterTwo', 'ctrl+alt+2');
        handler.registerShortcut('letterThree', 'ctrl+alt+3');
        handler.registerShortcut('letterFour', 'ctrl+alt+4');
        handler.registerShortcut('letterFive', 'ctrl+alt+5');

        // Send key events on the English (United States) layout.
        fireAltGraphKey(KeyCodes.ONE, 0, {ctrlKey: true, altKey: true});
        fireAltGraphKey(KeyCodes.TWO, 0, {ctrlKey: true, altKey: true});
        fireAltGraphKey(KeyCodes.THREE, 0, {ctrlKey: true, altKey: true});
        fireAltGraphKey(KeyCodes.FOUR, 0, {ctrlKey: true, altKey: true});
        fireAltGraphKey(KeyCodes.FIVE, 0, {ctrlKey: true, altKey: true});

        listener.$verify()
      }
    }

    function testAltGraphKeyOnFrenchLayout() {
      // Windows assigns printable characters to ctrl+alt+[2-5] keys of the
      // French layout. This test verifies we fire shortcut events only when
      // we type ctrl+alt+1 keys on the French layout.
      listener.shortcutFired('letterOne');
      if (goog.userAgent.WINDOWS && !goog.userAgent.GECKO) {
        listener.$replay();

        handler.registerShortcut('letterOne', 'ctrl+alt+1');
        handler.registerShortcut('letterTwo', 'ctrl+alt+2');
        handler.registerShortcut('letterThree', 'ctrl+alt+3');
        handler.registerShortcut('letterFour', 'ctrl+alt+4');
        handler.registerShortcut('letterFive', 'ctrl+alt+5');

        // Send key events on the French (France) layout.
        fireAltGraphKey(KeyCodes.ONE, 0, {ctrlKey: true, altKey: true});
        fireAltGraphKey(KeyCodes.TWO, 0x0303, {ctrlKey: true, altKey: true});
        fireAltGraphKey(KeyCodes.THREE, 0x0023, {ctrlKey: true, altKey: true});
        fireAltGraphKey(KeyCodes.FOUR, 0x007b, {ctrlKey: true, altKey: true});
        fireAltGraphKey(KeyCodes.FIVE, 0x205b, {ctrlKey: true, altKey: true});

        listener.$verify()
      }
    }

    function testAltGraphKeyOnSpanishLayout() {
      // Windows assigns printable characters to ctrl+alt+[1-5] keys of the
      // Spanish layout. This test verifies we do not fire shortcut events at
      // all when typing ctrl+alt+[1-5] keys on the Spanish layout.
      if (goog.userAgent.WINDOWS && !goog.userAgent.GECKO) {
        listener.$replay();

        handler.registerShortcut('letterOne', 'ctrl+alt+1');
        handler.registerShortcut('letterTwo', 'ctrl+alt+2');
        handler.registerShortcut('letterThree', 'ctrl+alt+3');
        handler.registerShortcut('letterFour', 'ctrl+alt+4');
        handler.registerShortcut('letterFive', 'ctrl+alt+5');

        // Send key events on the Spanish (Spain) layout.
        fireAltGraphKey(KeyCodes.ONE, 0x007c, {ctrlKey: true, altKey: true});
        fireAltGraphKey(KeyCodes.TWO, 0x0040, {ctrlKey: true, altKey: true});
        fireAltGraphKey(KeyCodes.THREE, 0x0023, {ctrlKey: true, altKey: true});
        fireAltGraphKey(KeyCodes.FOUR, 0x0303, {ctrlKey: true, altKey: true});
        fireAltGraphKey(KeyCodes.FIVE, 0x20ac, {ctrlKey: true, altKey: true});

        listener.$verify()
      }
    }

    function testNumpadKeyShortcuts() {
      var testCases = [
        ['letterNumpad0', 'num-0', KeyCodes.NUM_ZERO],
        ['letterNumpad1', 'num-1', KeyCodes.NUM_ONE],
        ['letterNumpad2', 'num-2', KeyCodes.NUM_TWO],
        ['letterNumpad3', 'num-3', KeyCodes.NUM_THREE],
        ['letterNumpad4', 'num-4', KeyCodes.NUM_FOUR],
        ['letterNumpad5', 'num-5', KeyCodes.NUM_FIVE],
        ['letterNumpad6', 'num-6', KeyCodes.NUM_SIX],
        ['letterNumpad7', 'num-7', KeyCodes.NUM_SEVEN],
        ['letterNumpad8', 'num-8', KeyCodes.NUM_EIGHT],
        ['letterNumpad9', 'num-9', KeyCodes.NUM_NINE],
        ['letterNumpadMultiply', 'num-multiply', KeyCodes.NUM_MULTIPLY],
        ['letterNumpadPlus', 'num-plus', KeyCodes.NUM_PLUS],
        ['letterNumpadMinus', 'num-minus', KeyCodes.NUM_MINUS],
        ['letterNumpadPERIOD', 'num-period', KeyCodes.NUM_PERIOD],
        ['letterNumpadDIVISION', 'num-division', KeyCodes.NUM_DIVISION]
      ];
      for (var i = 0; i < testCases.length; ++i) {
        listener.shortcutFired(testCases[i][0]);
      }
      listener.$replay();

      // Register shortcuts for numpad keys and send numpad-key events.
      for (var i = 0; i < testCases.length; ++i) {
        handler.registerShortcut(testCases[i][0], testCases[i][1]);
        fire(testCases[i][2]);
      }
      listener.$verify()
    }

  </script>
</body>
</html>
